iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0
Software Development

程式基礎概念討論系列 第 23

[DAY 23] 抽象化讓我們快速知道類別的功能

  • 分享至 

  • xImage
  •  

在開始今天的討論前,讓我們先來看一下昨天最後的問題:

假如現在我們要除了定義鴨子跟老鷹的類別外,還要定義玩具飛機跟玩具輪船的類別:

老鷹類別(Eagle):[變數] 名稱(name) [函式] 鳴叫(speak) 飛行(fly)
鴨子類別(Duck):[變數] 名稱(name) [函式] 鳴叫(speak) 游泳(swim)
玩具飛機類別(ToyPlane):[變數] 價格(price) [函式] 充電(charge) 飛行(fly)
玩具輪船類別(ToyShip):[變數] 價格(price) [函式] 充電(charge) 游泳(swim)

我們可以看到每個類別都會跟至少兩個不同的類別有著相同的變數或函式,那我們該怎麼做呢?

首先,必須要提醒大家一點的是,很多問題其實並沒有所謂的標準答案,因此筆者的想法也只是個人習慣的做法,並不一定代表是比較好的做法。那在上面的情況下,我會建議大家抽取最多共同點的部分來進行整合,例如:老鷹類別跟鴨子類別有兩個相同的元素 (變數或函式),跟玩具飛機類別卻只有一個相同的元素;另一方面,玩具飛機類別也是跟玩具輪船類別有兩個相同的元素。考慮到減少重複性的想法,我會為老鷹類別跟鴨子類別整合一個父類別 [鳥類類別],為玩具飛機類別跟玩具輪船類別整合另一個父類別 [玩具類別]:

鳥類類別(Bird):[變數] 名稱(name) [函式] 鳴叫(speak)
玩具類別(Toy):[變數] 價格(price) [函式] 充電(charge)
老鷹類別(Eagle) 繼承 鳥類類別(Bird):[函式] 飛行(fly)
鴨子類別(Duck) 繼承 鳥類類別(Bird):[函式] 游泳(swim)
玩具飛機類別(ToyPlane) 繼承 玩具類別(Toy):[函式] 飛行(fly)
玩具輪船類別(ToyShip) 繼承 玩具類別(Toy):[函式] 游泳(swim)

那如果每個類別都只有一個元素是重複的又如何呢?這時候我們就要考慮它們在未來更新的可能性了,例如在之後新增的類別會不會也有相同的元素,這時侯可以把這些可能會在以後更新會使用的元素優先整合出一個父類別。

不過,有時候我們在整理程式碼除了需要考慮減少重複性外,還有一個很重要的事情應該去考慮的,那就是易讀性。透過把程式碼變得更容易閱讀,雖然對我們執行程式的部分不一定會比較有幫助,但這可以讓我們更容易掌握及理解隨著開發的過程變得越來越複雜的程式碼,使我們開發的過程變得順利一點。而在物件導向中,輔助我們提升程式易讀性的方法就是抽象化 (Abstraction)

抽象化的概念是準備一些不能被直接使用的抽象類別 (Abstract Class)介面 (Interface),並讓子類別在繼承它們時才把抽象類別中的抽象部分或介面的內容實作出來,因此,我們也會把繼承介面的行為稱為實作 (Implement)。那它們跟一般繼承的父類別有什麼不一樣了?那就是「抽象的部分」是不存在的,因此繼承後我們還要重新把它做出來。我們可以把這狀況理解為生物的定義跟描述的差異,定義是非常準確的內容的,因此我們可以在定義中知道那個生物實際上會有什麼特徵跟行為;可是,描述就不一定準確了,它可能是一種模糊的、大概的感覺,因此我們在描述中知道那個生物可能會有什麼特徵跟行為,可是卻不知道牠實際的狀況是如何,像是雖然知道牠會攻擊人,卻不知道是怎麼攻擊。因此當我們要定義一個乎合該描述的生物時,便需要重新把描述的部分定義清楚。而抽象類別跟介面的差異就是,抽象類別可以包含有實際定義的變數及函式,而介面則不可以。舉例來說,我們讓老鷹類別實作了 可飛行(Flyable) 的介面,由於可飛行介面中不可以有實際的定義,我們會看到裡面的函式是沒有內容的,因此我們要在老鷹類別中把可飛行介面中的函式做出來:[C#]

using System;

public interface Flyable { // 介面
    public void fly(); // 這是一個沒有內容的函式
}

public class Eagle : Flyable { // 繼承介面
    public string name;
    public Eagle() {
        name = "Eagle";
    }
    public void speak() {
        ...
    }
    public void fly() { // 介面中 "描述" 了 fly 函式,因此我們要把  fly 函式實際的功能做出來
        Console.WriteLine(name + " Fly!");
    }
}

public class InterfaceExample
{
    public static void Main(string[] args)
    {
        Eagle eagle = new Eagle();
        eagle.fly(); // 顯示:Eagle Fly!
    }
}

看上面的例子的話,我們會發現抽象化不但沒有減少我們重複的程式碼,還讓我們需要增加了「多餘的程式碼」,那我們為什麼還需要它了?

1. 子類別只能繼承一個有實際內容的類別,卻可以實作無數個介面
子類別只能繼承一個父類別的原因是為了避免在兩個或以上的父類別中的定義出現了衝突,使子類別不知道應該跟隨哪一個類別中的定義,例如類別 A 跟類別 B 都有一個功能是用來打招呼的,可是類別 A 的打招呼功能是說「你好」,而類別 B 的打招呼功能是說「Hello」,那如果類別 C 同時繼承了類別 A 跟類別 B,它的打招呼功能應該是「你好」還是「Hello」呢?相對地,由於介面沒有實際的內容,因此同時實作多個介面也不會發生同樣的衝突,所以在一個子類別中可以實作的介面數並沒有受到限制。

2. 介面幫助我們為類別進行分類
那介面沒有實際的內容的話,它到底有什麼用途呢?那就是幫助我們分類,雖然介面並沒有內容,但它仍然描述了實作它的類別會有什麼功能。因此我們可以透過介面來把擁有類似功能的類別分類在一起,這樣一來我們便可以透過實作的介面知道不同的類別之間有沒有類似的功能。

3. 介面幫助我們為類別加上了限制
我們需要注意的是,介面中描述的函式是必須在實作它的類別中為那些函式補上內容的。因此,透過實作介面,我們便可以確定類別中必定會擁有什麼功能。

從變數到屬性

從上文中,我們反複強調了介面是不可以有實際內容的,可是,矛盾的是,對程式來說變數本身就是一種實際的內容。一旦在介面中存在著變數的話,它便違反了介面的原則,因此我們一般不會在介面中加入任何的變數。可是,如果我們真的很需要為介面加上變數,又有沒有辦法呢?有的,答案就是讓變數變得「對程式來說看起來跟函式一樣」,也就是在變數中補上存取的函式 (Getter & Setter),這種帶有函式的變數便被稱為屬性 (Property)

介於類別與介面之間的抽象類別

介面的特點在於它並沒有實際內容,因此它不能在程式中直接建立出來。那我們可不可以準備一個帶有實際內容的介面呢?可以,那就是抽象類別。抽象類別指的就是可以包含沒有實際內容的函式的類別,可是,由於它可以存在實際內容,因此它也必須遵守只能繼承一個的原則。那它有什麼用途呢?

1. 我們可以保留函式的實作部分
有時候繼承同一個父類別的子類別雖然有著類似的功能,但實際的執行方法卻各不一樣。透過抽象類別描述這些相似的函式並保留至子類別才實作的話,我們便可以省下增加一個新的介面的步驟了。

2. 我們可以避免這個類別被建立為物件
有時候我們並不希望一些父類別在程式執行時被建立為物件,例如上面例子中的鳥類類別跟玩具類別,它們的目的是把類似的類別整合在一起而不是作為一個獨立的類別存在。因此這時候我們便可以把它們定義為抽象類別,使它們跟介面一樣不能在程式執行時直接建立為物件。這便可以避免我們不小心把它建立為物件使用導致出現非預期的結果。

在了解了抽象化後,我們便可以把上面的例子稍作改良,使它的類別與分類可以看起來更條理與清晰:

抽象類別
鳥類類別(Bird):[變數] 名稱(name) [函式] 鳴叫(speak)
玩具類別(Toy):[變數] 價格(price) [函式] 充電(charge)

介面
可飛行(Flyable):[函式] 飛行(fly)
可游泳(Swimmable):[函式] 游泳(swim)

目標類別
老鷹類別(Eagle) 繼承 鳥類類別(Bird) 實作 可飛行(Flyable)
鴨子類別(Duck) 繼承 鳥類類別(Bird) 實作 可游泳(Swimmable)
玩具飛機類別(ToyPlane) 繼承 玩具類別(Toy) 實作 可飛行(Flyable)
玩具輪船類別(ToyShip) 繼承 玩具類別(Toy) 實作 可游泳(Swimmable)

而在程式中則是:[C#]

using System;

public abstract class Bird { // 抽象類別
    public string name;
    public void speak() { // 抽象類別中仍然可以定義有內容的函式
        Console.WriteLine(name + " Quack!");
    }
}

public abstract class Toy { // 抽象類別
    public int price;
    public void charge() {
        Console.WriteLine("Used " + price.ToString() + " And Charging!");
    }
}

public interface Flyable { // 介面
    public void fly();
}

public interface Swimmable { // 介面
    public void swim();
}

public class Eagle : Bird, Flyable {
    public Eagle() { // 建構子
        name = "Eagle";
    }
    public void fly() { // 實作介面描述的函式
        Console.WriteLine(name + " Fly!");
    }
}

public class Duck : Bird, Swimmable {
    public Duck() { // 建構子
        name = "Duck";
    }
    public void swim() { // 實作介面描述的函式
        Console.WriteLine(name + " Swim!");
    }
}

public class ToyPlane : Toy, Flyable {
    public ToyPlane() { // 建構子
        price = 100;
    }
    public void fly() { // 實作介面描述的函式
        Console.WriteLine("Used " + price.ToString() + " And Flying!");
    }
}

public class ToyShip : Toy, Swimmable {
    public ToyShip() { // 建構子
        price = 50;
    }
    public void swim() { // 實作介面描述的函式
        Console.WriteLine("Used " + price.ToString() + " And Swimming!");
    }
}

public class ClassAndInterfaceExample
{
    public static void Main(string[] args)
    {
        Eagle eagle = new Eagle();
        eagle.speak(); // 顯示:Eagle Quack!
        eagle.fly(); // 顯示:Eagle Fly!
        
        Duck duck = new Duck();
        duck.speak(); // 顯示:Duck Quack!
        duck.swim(); // 顯示:Duck Swim!
        
        ToyPlane toyPlane = new ToyPlane();
        toyPlane.charge(); // 顯示:Used 100 And Charging!
        toyPlane.fly(); // 顯示:Used 100 And Flying!
        
        ToyShip toyShip = new ToyShip();
        toyShip.charge(); // 顯示:Used 50 And Charging!
        toyShip.swim(); // 顯示:Used 50 And Swimming!
    }
}

上一篇
[DAY 22] 繼承把物件重複的定義都整合在一起
下一篇
[DAY 24] 多型讓類別的內容有更多變化
系列文
程式基礎概念討論30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言